4. Processing the Dead-Letter Queue
The client needs to somehow process the accumulated messages in
the DLQ. In the case of the system-wide DLQ, the client can provide a
mega-service that supports all contracts of all queued endpoints on
the system to enable it to process all failed messages. This is
clearly an impractical idea, though, because that service could not
possibly know about all queued contracts, let alone provide meaningful
processing for all applications. The only feasible way to make this
solution work would be to restrict the client side to at most a single
queued service per system. Alternatively, you can write a custom
application for direct administration and manipulation of the system
DLQ using the types in System.Messaging. That application will
parse and extract the relevant messages and process them. The problem
with that approach (besides the inordinate amount of work involved) is
that if the messages are protected and encrypted (as they should be),
the application will have a hard time dealing with and distinguishing
between them. In practical terms, the only possible solution for a
general client-side environment is the one offered by MSMQ 4.0: a
custom DLQ. When using a custom DLQ, you also provide a client-side
service whose queue is the application’s custom DLQ. That service will
process the failed messages according to the application-specific
requirements.
4.1. Defining the DLQ service
Implementing the DLQ service is done like any other queued
service. The only requirement is that the DLQ service be polymorphic
with the original service’s contract. If multiple queued endpoints
are involved, you will need a DLQ per contract per endpoint. Example 2 shows a possible setup.
Example 2. DLQ service config file
<!-- Client side --> <system.serviceModel> <client> <endpoint address = "net.msmq://localhost/private/MyServiceQueue" binding = "netMsmqBinding" bindingConfiguration = "MyCustomDLQ" contract = "IMyContract" /> </client> <bindings> <netMsmqBinding> <binding name = "MyCustomDLQ" deadLetterQueue = "Custom" customDeadLetterQueue = "net.msmq://localhost/private/MyCustomDLQ"> </binding> </netMsmqBinding> </bindings> </system.serviceModel>
<!-- DLQ service side --> <system.serviceModel> <services> <service name = "MyDLQService"> <endpoint address = "net.msmq://localhost/private/MyCustomDLQ" binding = "netMsmqBinding" contract = "IMyContract" /> </service> </services> </system.serviceModel>
|
The client config file defines a queued endpoint with the
IMyContract contract. The client
uses a custom binding section to
define the address of the custom DLQ. A separate queued service
(potentially on a separate machine) also supports the IMyContract contract.
The DLQ service uses as its address the DLQ defined by the
client.
4.2. Failure properties
The DLQ service typically needs to know why the queued call
delivery failed. WCF therefore offers the MsmqMessageProperty
class, used to find out the cause of the failure and the current
status of the message. MsmqMessageProperty is defined in the
System.ServiceModel.Channels namespace:
public sealed class MsmqMessageProperty
{
public const string Name = "MsmqMessageProperty";
public int AbortCount
{get;}
public DeliveryFailure? DeliveryFailure
{get;}
public DeliveryStatus? DeliveryStatus
{get;}
public int MoveCount
{get;}
//More members
}
The DLQ service needs to obtain the MsmqMessageProperty from the operation
context’s incoming message properties:
public sealed class OperationContext : ...
{
public MessageProperties IncomingMessageProperties
{get;}
//More members
}
public sealed class MessageProperties : IDictionary<string,object>,...
{
public object this[string name]
{get;set;}
//More members
}
When a message is passed to the DLQ, WCF will add to its
properties an instance of MsmqMessageProperty detailing the failure.
MessageProperties is merely a
collection of message properties that you can access using a string
as a key. To obtain the MsmqMessageProperty, use the constant MsmqMessageProperty.Name, as shown in
Example 3.
Example 3. Obtaining the MsmqMessageProperty
[ServiceContract(SessionMode = SessionMode.NotAllowed)] interface IMyContract { [OperationContract(IsOneWay = true)] void MyMethod(string someValue); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyDLQService : IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod() { MsmqMessageProperty msmqProperty = OperationContext.Current. IncomingMessageProperties[MsmqMessageProperty.Name] as MsmqMessageProperty;
Debug.Assert(msmqProperty != null); //Process msmqProperty } }
|
Note in Example 3
the use of the practices discussed so far for configuring the
session mode, instance management, and transactions—the DLQ service
is, after all, just another queued service.
The properties of MsmqMessageProperty detail the reasons for
failure and offer some contextual information. MoveCount is the number of attempts made
to play the message to the service, and AbortCount is the number of attempts made
to read the message from the queue. AbortCount is less relevant to recovery
attempts, because it falls under the responsibility of MSMQ and
usually is of no concern. DeliveryStatus is a nullable enum of the
type DeliveryStatus, defined
as:
public enum DeliveryStatus
{
InDoubt,
NotDelivered
}
When a regular WCF queued service processes a delivered call,
DeliveryStatus is set to null. With a DLQ service, DeliveryStatus will be set to DeliveryStatus.InDoubt unless the message
was positively not delivered (i.e., a NACK was received). For
example, expired messages are considered in-doubt because their
time to live elapsed before the service could acknowledge them one
way or the other.
The DeliveryFailure
property is a nullable enum of the type DeliveryFailure, defined as follows
(without the specific numerical values):
public enum DeliveryFailure
{
AccessDenied,
NotTransactionalMessage,
Purged,
QueueExceedMaximumSize,
ReachQueueTimeout,
ReceiveTimeout,
Unknown
//More members
}
Note:
When a regular WCF queued service processes a queued call
and access MsmqMessageProperty,
both DeliveryStatus and
DeliveryFailure are set to
null.
4.3. Implementing a DLQ service
The DLQ service cannot affect a message’s properties (for
example, extending its time to live). Handling of delivery failures
typically involves some kind of compensating workflow: notifying the
administrator; trying to resend a new message, or resending a new
request with extended timeout; logging the error; or perhaps doing
nothing (i.e., merely processing the failed call and returning, thus
discarding the message).
Example 4 demonstrates
a possible DLQ service implementation.
Example 4. Implementing a DLQ service
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyDLQService : IMyContract { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod(string someValue) { MsmqMessageProperty msmqProperty = OperationContext.Current. IncomingMessageProperties[MsmqMessageProperty.Name] as MsmqMessageProperty; //If tried more than 25 times: discard message if(msmqProperty.MoveCount >= 25) { return; } //If timed out: try again if(msmqProperty.DeliveryStatus == DeliveryStatus.InDoubt) { if(msmqProperty.DeliveryFailure == DeliveryFailure.ReceiveTimeout) { MyContractClient proxy = new MyContractClient(); proxy.MyMethod(someValue); proxy.Close(); } return; } if(msmqProperty.DeliveryStatus == DeliveryStatus.InDoubt || msmqProperty.DeliveryFailure == DeliveryFailure.Unknown) { NotifyAdmin(); } } void NotifyAdmin() {...} }
|
The DLQ service in Example 4 examines the cause of
the failure. If WCF has tried more than 25 times to deliver the
message, the DLQ service simply gives up and drops the message. If
the cause for the failure was a timeout, the DLQ service tries again
by creating a proxy to the queued service and calling it, passing
the same arguments from the original call (the in-parameters to the
DLQ service operation). If the message is in-doubt or an unknown
failure took place, the service notifies the application administrator.